Skip to content
概述
objDiff 接收两个 JSON 兼容的值(对象、数组、基本类型或 null),返回一个仅包含差异部分的“差异对象”。相等的属性会被压缩掉,类型不一致的地方会以 [原值, 新值] 对的形式保留,共同结构之外的键不参与比较。
基本概念
返回结果遵循三条规则:
- 类型不同 → 返回
[obj1, obj2]。这样调用方可以直接知道“从什么变成了什么”,而不仅得到一个类型不匹配的标记。 - 基本类型相同且值相等 → 返回空对象
{}。在向上递归时,空对象的差异会被过滤掉,最终结果里不会出现实际上无变化的键。 - 只比较共同键。只在
obj1或只在obj2中出现的键不会进入差异结果。本函数关注的是“共同结构里的差异”,而非完整的增删改对比。
差异结果的结构与原对象对应:对于有差异的嵌套属性,会递归生成子差异对象,最终得到一个裁剪后的镜像。
工作原理
- 使用
Object.prototype.toString精确判断类型(区分数组和普通对象)。 - 如果
obj1与obj2类型不同,直接返回[obj1, obj2]。 - 如果两者都不是对象(
null、基本类型),直接做相等比较:相等返回{},不相等返回[obj1, obj2]。 - 对于对象/数组,取
obj1的键,过滤出obj2也拥有的键(Object.hasOwn检查自身属性)。 - 对这些共同键递归调用
objDiff,将得到的差异对象中非空的部分收集起来,构成顶层差异对象。 - 返回最终的差异对象。
递归过程中空对象的过滤保证了结果只保留实际有变化的路径。
基本用法
函数签名大致如下(TypeScript 可声明类型,这里用 JavaScript 实现并辅以注释说明类型约束):
js
/**
* @param {*} obj1
* @param {*} obj2
* @returns {Object}
*/
function objDiff(obj1, obj2) {
const isObject = (obj) => typeof obj === 'object' && obj !== null;
const type = (obj) => Object.prototype.toString.call(obj).slice(8, -1);
// 类型不同
if (type(obj1) !== type(obj2)) return [obj1, obj2];
// 非对象(基本类型 / null)
if (!isObject(obj1)) return obj1 === obj2 ? {} : [obj1, obj2];
// 对象 / 数组:只取共同键
const keys = Object.keys(obj1).filter((k) => Object.hasOwn(obj2, k));
const diff = {};
for (const k of keys) {
const sub = objDiff(obj1[k], obj2[k]);
if (Object.keys(sub).length > 0) {
diff[k] = sub;
}
}
return diff;
}调用时直接传入两个值即可:
js
const a = { x: 1, y: 2 };
const b = { x: 1, y: 3 };
objDiff(a, b);示例
所有示例均在 Node.js 环境下运行。
1. 两个完全相等的对象
js
const obj1 = { name: 'Alice', age: 30 };
const obj2 = { name: 'Alice', age: 30 };
console.log(objDiff(obj1, obj2)); // {}2. 部分属性值不同
js
const obj1 = { name: 'Alice', age: 30 };
const obj2 = { name: 'Alice', age: 31 };
console.log(objDiff(obj1, obj2));
// { age: [30, 31] }name 相等,被过滤;age 不等,以 [旧值, 新值] 形式出现在结果中。
3. 值类型不同
js
console.log(objDiff(42, '42'));
// [42, '42']基本类型类型不同(Number vs String)同样返回差异对。
4. 嵌套对象有差异
js
const obj1 = { user: { name: 'Alice', email: 'a@b.com' } };
const obj2 = { user: { name: 'Bob', email: 'a@b.com' } };
console.log(objDiff(obj1, obj2));
// { user: { name: ['Alice', 'Bob'] } }email 相等被忽略,只有 name 出现在差异中。
5. 一方多出的键不计入差异
js
const obj1 = { x: 1, y: 2 };
const obj2 = { x: 1 };
console.log(objDiff(obj1, obj2)); // {}y 不在 obj2 中,不在共同键范围内,因此结果为空。若需要增删检测,需结合另外的方法。
6. 数组作为对象处理
js
const arr1 = [1, 2, 3];
const arr2 = [1, 4, 3];
console.log(objDiff(arr1, arr2));
// { '1': [2, 4] }数组被视为以索引为键的对象,差异结果中使用字符串键 '1' 表示第二个元素不同。
注意点
- 空对象表示无差异。收到
{}意味着所有共同部分相同,不存在需要关注的变化。 null是对象? 代码中用typeof obj === 'object' && obj !== null排除null,null被视为基本类型,与其他基本类型比较时直接比较值。Object.hasOwn替代hasOwnProperty。直接使用Object.hasOwn(obj2, key)更简洁,也避免了自己或原型链上hasOwnProperty被覆盖的风险。- 类型判断开销。
Object.prototype.toString比typeof更精确,但每次调用都会进行字符串切片。如果递归层数极深且键数量庞大,可将类型结果缓存以避免重复调用。
限制
- 只比较自身属性,不涉及原型链。
- 键顺序不影响比较,因为对象键的差异基于键名,而非顺序。
- 不适合直接用于大对象的实时同步场景——如果对象顶层键数量很大,
Object.keys(obj1).filter和后续递归会放大计算量。此时可考虑结合Set快速计算键的交集。 - 返回的差异对象中,值可能是原始值、数组(差异对)或嵌套差异对象,调用方需根据结构自行解析。
应用
- 配置版本对比:比较前后两份配置,快速定位修改项。
- JSON 补丁生成前的预处理:先提取共同结构中的差异,再结合增删检测生成完整补丁。
- 数据同步校验:只关心重叠字段是否一致,而不想被仅在一端存在的新字段干扰。
